"""Particle groups"""

__version__ = '$Id: _group.py 109 2008-07-26 07:04:48Z casey.duncan $'

default_system_marker = object()
not_specified = object()

class ParticleGroup(object):

	# Subclasses may specify controllers
	# as well as passing them into the constructor
	# The use of a tuple for controllers is to prevent
	# mutation in-place so that changes to it can be
	# safely made during iteration (e.g., during update())
	controllers = ()

	def __init__(self, controllers=(), renderer=None, system=default_system_marker):
		"""Initialize the particle group, binding the supplied 
		controllers to it and setting the renderer.
		
		If a system is specified, the group is added to that particle system
		automatically. By default, the group is added to the default particle
		system (particle.default_system). If you do not wish to bind the group to a
		system immediately, pass None for the system.
		"""
		self.particles = set()
		self.bind_controller(*controllers)
		self.renderer = None
		if renderer is not None:
			self.set_renderer(renderer)
		if system is default_system_marker:
			# late binding in case the default system is replaced
			from particle import default_system
			default_system.add_group(self)
		elif system is not None:
			system.add_group(self)
	
	def bind_controller(self, *controllers):
		"""Bind one or more controllers to the group (see update() below)"""
		self.controllers += controllers
	
	def unbind_controller(self, controller):
		"""Unbind a controller from the group so it is no longer
		invoked on update. If the controller is not bound to the group
		raise ValueError
		"""
		# This is a bit tortured, but we do this because the controller
		# list cannot be mutated in place to avoid race conditions
		ctrl_list = list(self.controllers)
		ctrl_list.remove(controller)
		self.controllers = tuple(ctrl_list)
	
	def set_renderer(self, renderer):
		"""Set the renderer for the group. This replaces any existing
		renderer. Pass None to disable rendering
		"""
		if renderer is not None:
			renderer.set_group(self)
		if self.renderer is not None:
			self.renderer.set_group(None)
		self.renderer = renderer

	def new(self, particle):
		"""Add a particle to the group. 
		
		This method is typically called by particle emitters
		"""
		self.particles.add(particle)
		return particle
	
	def kill(self, particle):
		"""Remove a particle from the group. Raise ValueError if the particle is
		not in the group.

		This is typically called by controllers.
		"""
		try:
			self.particles.remove(particle)
		except KeyError:
			raise ValueError, ('%s.remove(particle): Particle not in group'
				% self.__class__.__name__)

	def __iter__(self):
		"""Iterate the particles in the group"""
		# Make a copy of the particle set in case the group is
		# modified during iteration
		return iter(set(self.particles))
	
	def __len__(self):
		"""Return the number of particles in the group"""
		return len(self.particles)
	
	def update(self, time_delta):
		"""Update all particles in the group by applying the bound controllers
		to them in succession. The controllers are applied in the order they
		were bound to the group
		"""
		# Set the last postions and velocities of the particles
		# This needs to happen exactly once each update
		# So we need to do it ourself to guarantee that
		# happens once and only once
		for particle in self.particles:
			particle.age += time_delta
			particle.last_position = particle.position
			particle.last_velocity = particle.velocity
		# Invoke the controllers
		if self.system is not None:
			controllers = self.system.controllers + self.controllers
		for controller in controllers:
			controller(time_delta, self)
	
	def draw(self):
		"""Draw all particles in the group using the bound renderers. 
		The renderers are used in the order they were bound to the group
		"""
		if self.renderer is not None:
			self.renderer.draw()
